#!/usr/bin/env python

"""
Library that provides a set of classes that work with the Pownce API

The Python Pownce library is a collaborative work and is successful do
to the help of many people.

Please see the AUTHORS.txt file for a complete list of contributors.

Details on the Pownce API can be found on the following sites:

    Pownce Community Wiki   http://pownce.pbwiki.com/API+Documentation2-0
    Pownce Google Group     http://groups.google.com/group/pownceapi

Requires:

    python 2.5+             http://python.org
    httplib2                http://bitworking.org/projects/httplib2
    simplejson              http://cheeseshop.python.org/pypi/simplejson
    feedparser              http://feedparser.org
"""

__version__ = '0.3'
__license__ = """
Copyright (c) 2007,2008 Clint Ecker and Mike Taylor
All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import os, sys
import time
import random
import urllib
import httplib2
import mimetypes
import email.encoders
import email.mime.base
import hashlib, base64, binascii

import xml.etree.ElementTree
#import feedparser
import simplejson


_debug = False

def _pdump(title, data):
    if _debug:
        print title
        pp.pprint(data)
        print '-'*42
    else:
        pass

_userAgent = 'PyPownce/%s +http://code.google.com/p/pownceapi/' % __version__
_apiFormat = 'json'
_apiRoot   = 'http://api.pownce.com/2.0'

MIME_BOUNDARY = '--------------pownce_magical_boundary_value$'


class UserList(object):
    def __init__(self, usertype, session):
        self.usertype = usertype
        self.session  = session

        self._clear()
        self.refresh()

    def refresh(self, fresh=False):
        if fresh:
            self.page = 0
        else:
            self.page += 1

        url = '%s/users/%s/%s.%s' % (_apiRoot, self.session.userid, self.usertype, _apiFormat)
        status, data = self.session._pull(url, page=self.page)

        self._load(data, status)

    def _dump(self):
        _pdump('%s status' % self.usertype, self.status)
        _pdump('%d total' % self.count, 'page %d, next=%s, prev=%s' % (self.page, self.nextPage, self.prevPage))

        for item in self.list:
            _pdump('%s list' % item.username, item._dump())

    def _clear(self):
        self.data     = None
        self.status   = None
        self.list     = []
        self.page     = 0
        self.count    = 0
        self.nextPage = False
        self.prevPage = False

    def _load(self, data, status):
        self._clear()

        self.data   = data
        self.status = status

        if self.usertype in data:
            item = data[self.usertype]

            self.count    = item['total_count']
            self.prevPage = item['has_prev_page']
            self.nextPage = item['has_next_page']

            for user in item['users']:
                p = Profile(self.session)
                p._load(user, status)

                self.list.append(p)


class Profile(object):
    def __init__(self, session):
        self.session = session

        self._clear()

    def _dump(self):
        _pdump('%s status' % self.username, self.status)
        _pdump('%s data' % self.username, self.data)

    def _clear(self):
        self.data    = None
        self.status  = None
        self.friends = None
        self.fan_of  = None
        self.fans    = None

    def _load(self, data, status):
        self._clear()

        self.data   = data
        self.status = status

        for field in data:
            setattr(self, field, data[field])

    def refresh(self, pullLists=False):
        """
        Retrieve a user's profile information
        """
        status, data = self.session._pull('%s/users/%s.%s' % (_apiRoot, self.session.userid, _apiFormat))

        self._load(data, status)

        if pullLists:
            if self.friends is None:
                self.getFriends()

            if self.fans is None:
                self.getFans()

            if self.fan_of is None:
                self.getFanOf()

    def getFriends(self, fresh=False):
        """
        Build the list of sets and friends
        """
        if self.friends is None:
            self.friends = UserList('friends', self.session)
        else:
            self.friends.refresh(fresh=fresh)

    def getFanOf(self, fresh=False):
        """
        Retrieve the list of people the user is a fan of
        """
        if self.fan_of is None:
            self.fan_of = UserList('fan_of', self.session)
        else:
            self.fan_of.refresh(fresh=fresh)

    def getFans(self, fresh=False):
        """
        Retrieve the list of fans
        """
        if self.fans is None:
            self.fans = UserList('fans', self.session)
        else:
            self.fans.refresh(fresh=fresh)


class Session(object):
    """
    A single Pownce session.
    
    Each instance of the Session class represents a single Pownce session and
    maintains all of the necessary state information from the initial login,
    any posts or reads, and on to the final logout.
    
    If specified, it will use a cache directory to store any intermediate files
    that are seen during the current session.
    """
    def __init__(self, cachedir=None, app_key=None):
        self.userid    = None
        self.password  = None
        self.app_key   = app_key
        self.apitoken  = None
        self.sessionid = None
        self.sets      = None

        if cachedir is None:
            self.cachedir = os.path.abspath(os.path.expanduser('~/.cache'))
        else:
            self.cachedir = cachedir

        self.http = httplib2.Http(self.cachedir)

        self.http.follow_redirects = True

    def _gen_wsse_auth(self):
        nonce  = hashlib.md5('%s:%s' % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])).hexdigest()[:16]
        c_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
        passwd = base64.b64encode(hashlib.sha1('%s%s%s' % (nonce, c_time, self.apitoken)).digest())

        return 'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"' % (self.userid, passwd, nonce, c_time)

    def _fetch(self, url, headers=None, post=None, multipart=False):
        hdrs = {}

        if headers is not None:
            for key in headers:
                hdrs[key] = headers[key]

        if 'UserAgent' not in hdrs:
            hdrs['User-Agent'] = _userAgent

        if self.auth is not None:
            hdrs['Authorization'] = self.auth

        # if self.sessionid is not None:
        #     hdrs['Cookie'] = 'sessionid=%s' % self.sessionid

        if post is not None:
            method = 'POST'
            #auth   = self._gen_wsse_auth()

            if multipart:
                body  = '%s\nContent-Disposition: form-data; name="auth"\n\n%s\n' % (MIME_BOUNDARY, self.auth)
                body += post

                hdrs['Content-Type']   = 'multipart/form-data; boundary=%s' % MIME_BOUNDARY
                hdrs['Content-Length'] = str(len(body))

                print body
            else:
                body = 'auth=%s&%s' % (self.auth, post)
        else:
            method = 'GET'
            body   = None

        _pdump('fetch %s headers' % method, hdrs)
        _pdump('fetch url', url)
        _pdump('fetch body', body)

        try:
            resp, content = self.http.request(url, method=method, body=body, headers=hdrs)

        except httplib2.HttpLib2Error, e:
            if hasattr(e, 'response'):
                resp    = e.response
                content = e.content
            else:
                resp    = {'status': '1'}
                content = e.desc

        _pdump('fetch response', resp)
        _pdump('fetch content', content)

        return resp, content

    def _pull(self, base_url, limit=20, page=0):
        json = {}
        url  = '%s?limit=%d&page=%d&app_key=%s' % (base_url, limit, page, self.app_key)

        response, content = self._fetch(url)

        if response['status'] == '200':
            json = simplejson.loads(content)

            # {"error": { "status_code": 401,
            #             "message":     "Missing required parameter: app_key",
            #             "request":     "http:\\/\\/api.pownce.com\\/2.0\\/users\\/bear.json"
            #           }
            # }

            if 'error' in json:
                status = (json['error']['status_code'], json['error']['message'])
            else:
                status = (200, 'OK')
        else:
            status = (response['status'], 'http error')

        return status, json

    def login(self, userid, password):
        self.userid   = userid
        self.password = password

        if userid is None or password is None:
            self.auth = None
        else:
            digest    = base64.encodestring('%s:%s' % (username, password))
            self.auth = 'Basic %s' % digest[:-1]

        # self.http.clear_credentials()
        # self.http.add_credentials(userid, password)
        #
        # # not sure if this is *really* needed, but for now...
        # resp, content = self._fetch('http://api.pownce.com/1.0/available.json')
        # 
        # # do a basic auth GET - the body contains user info
        # resp, content = self._fetch('http://api.pownce.com/1.0/login')
        # 
        # if 'set-cookie' in resp:
        #     try:
        #         self.sessionid = resp['set-cookie'].split(';')[0].split('=')[1]
        #     except:
        #         self.sessionid = None
        # 
        # dom = xml.etree.ElementTree.fromstring(content)
        # 
        # for element in dom.getiterator(): #'{http://pownce.com/Atom}login'):
        #     if element.tag == '{http://pownce.com/Atom}login':
        #         self.apitoken = element.attrib['token']
        #     elif element.tag == '{http://pownce.com/Atom}user':
        #         self.userinfo['pro'] = element.attrib['pro'] == '1'
        #     elif element.tag == '{http://pownce.com/Atom}name':
        #         self.userinfo['name'] = element.text
        #     elif element.tag == '{http://pownce.com/Atom}uri':
        #         self.userinfo['uri'] = element.text
        #     elif element.tag == '{http://pownce.com/Atom}image':
        #         self.userinfo['image'] = element.text
        # 
        # if self.apitoken is not None:
        #     self.http.add_credentials(userid, self.apitoken)


class Notes(object):
    """
    Helper class to manage the sending (push) and retrieving (pull)
    of all Pownce items for the user.
    
    Requires an active Pownce Session.
    """
    def __init__(self, session=None):
        self.session = session
        self.items   = []

    def refresh(self):
        """
        Pull the user's notes, parse and store in the feed item list.
        
        http://api.pownce.com/2.0/note_lists.{format}
        """
        status, data = self.session._pull('%s/note_lists/%s.%s' % (_apiRoot, self.session.userid, _apiFormat))

        _pdump('note_list', status)
        _pdump('note_list', data)

        # {u'notes': [{u'body': u"ok here is will.i.am doing a crap job of michael jackson's PYT.\r\ni mean come on... this is pathetic.\r\nwhat do u think?",
        #              u'display_since': u'23 min ago',
        #              u'file': {u'content_length': 5861268,
        #                        u'content_type': u'audio/mpeg',
        #                        u'name': u'12 PYT (Pretty Young Thing) 2008 (With Will.I.Am).mp3',
        #                        u'type': u'audio',
        #                        u'url': u'http://pownce.com/download/85597/12+PYT+%28Pretty+Young+Thing%29+2008+%28With+Will.I.Am%29.mp3/'},
        #              u'id': 1448300,
        #              u'is_private': False,
        #              u'is_public': False,
        #              u'num_recipients': u'1966',
        #              u'num_replies': u'2',
        #              u'permalink': u'http://pownce.com/mustard/notes/1448300/',
        #              u'seconds_since': 2830,
        #              u'sender': {u'age': 108,
        #                          u'blurb': u'the australian positive hip-hop headquarters! be our friend and get free music!',
        #                          u'country': u'Australia',
        #                          u'fan_count': 0,
        #                          u'fan_of_count': 7225,
        #                          u'first_name': u'mustard empire',
        #                          u'friend_count': 1964,
        #                          u'gender': u'None of the Above',
        #                          u'is_pro': False,
        #                          u'location': u'australia',
        #                          u'max_upload_mb': 10,
        #                          u'permalink': u'http://pownce.com/mustard/',
        #                          u'profile_photo_urls': {u'large_photo_url': u'http://pownce.com/profile_photos/m/u/s/mustard/17401_large.jpg',
        #                                                  u'medium_photo_url': u'http://pownce.com/profile_photos/m/u/s/mustard/17401_medium.jpg',
        #                                                  u'small_photo_url': u'http://pownce.com/profile_photos/m/u/s/mustard/17401_small.jpg',
        #                                                  u'smedium_photo_url': u'http://pownce.com/profile_photos/m/u/s/mustard/17401_smedium.jpg',
        #                                                  u'tiny_photo_url': u'http://pownce.com/profile_photos/m/u/s/mustard/17401_tiny.jpg'},
        #                          u'short_name': u'mustard empire ..',
        #                          u'username': u'mustard'},
        #              u'stars': u'0.0',
        #              u'timestamp': 1204533485,
        #              u'type': u'file'},

        if data is not None and 'notes' in data:
            for note in data['notes']:
                noteid   = note['id']
                notetype = note['type']

                if notetype == 'link':
                    item = Link(self.session)
                elif notetype == 'message':
                    item = Message(self.session)
                elif notetype == 'event':
                    item = Event(self.session)
                elif notetype == 'file':
                    item = File(self.session)
                else:
                    item = None

                if item is not None:
                    item.load(note)
                    self.items.append(item)


class Items(object):
    """
    Base class for all postable items.
    
    Message, Event, Link and File
    """
    def __init__(self, session):
        self.session = session
        self.type    = None
        self.body    = None
        self.to      = None

    def load(self, data):
        self._data = data

        for item in data.keys():
            if item == 'sender':
                p = Profile(self.session)
                p.refresh()
                self.sender = p
            else:
                setattr(self, item, data[item])

    def data(self):
        """
        Abstract method - must be overriden
        """
        raise


class Message(Items):
    """
    Represents a single Pownce Message type.
    """
    def __init__(self, session=None, text='', to=None):
        super(Message, self).__init__(session)

        self.type = 'note-message'
        self.body = text
        self.to   = to

    def load(self, data):
        super(Message, self).load(data)

    def data(self):
        """
        Format the post data string for a Message type
        """
        return 'note%%5Ftype=%s&note%%5Fbody=%s&note%%5Fto=%s' % (self.type, urllib.quote(self.body), self.to)


class Event(Items):
    """
    Represents a single Pownce Event type.
    """
    def __init__(self, session=None, text='', to=None, location='', name='', datetime=None):
        super(Event, self).__init__(session)

        self.type     = 'note-event'
        self.body     = text
        self.to       = to
        self.name     = name
        self.location = location

        if datetime is None:
            self.datetime = time.localtime()
        else:
            self.datetime = datetime

    def load(self, data):
        super(Event, self).load(data)

        item = data['event']

        self.name     = item['name']
        self.location = item['location']

    def data(self):
        """
        Format the post data string for an Event type
        """
        t  = time.strftime('%I:%M %p', self.datetime)

        s  = 'note%%5Ftype=%s&note%%5Fbody=%s&note%%5Fto=%s' % (self.type, urllib.quote(self.body), self.to)
        s += 'event%%5Fname=%s&&event%%5Flocation=%s&event%%5Ftime=%s&event%%5Fdate%5Fyear=%s&event%%5Fdate%5Fmonth=%s&event%%5Fdate%5Fday=%s' % \
             (self.name, self.location, t, self.datetime[0], self.datetime[1], self.datetime[2])

        return s


class Link(Items):
    """
    Represents a single Pownce Link type.
    """
    def __init__(self, session=None, text='', to=None, url=''):
        super(Link, self).__init__(session)

        self.type = 'note-link'
        self.body = text
        self.to   = to
        self.url  = url

    def load(self, data):
        super(Link, self).load(data)

        item = data['link']

        self.url = item['url']

    def data(self):
        """
        Format the post data string for an Link type
        """
        s = 'note%%5Ftype=%s&note%%5Fbody=%s&note%%5Fto=%s&url=%s' % (self.type, urllib.quote(self.body), self.to, self.url)

        return s


class File(Items):
    """
    Represents a single Pownce File type.
    """
    def __init__(self, session=None, text='', to=None, filename=''):
        super(File, self).__init__(session)

        self.type     = 'note-file'
        self.body     = text
        self.to       = to
        self.filename = filename
        self.mimetype = None

    def load(self, data):
        super(File, self).load(data)
        #              u'file': {u'content_length': 5861268,
        #                        u'content_type': u'audio/mpeg',
        #                        u'name': u'12 PYT (Pretty Young Thing) 2008 (With Will.I.Am).mp3',
        #                        u'type': u'audio',
        #                        u'url': u'http://pownce.com/download/85597/12+PYT+%28Pretty+Young+Thing%29+2008+%28With+Will.I.Am%29.mp3/'},

        item = data['file']

        self.filename = item['name']
        self.filetype = item['type']
        self.mimetype = item['content_type']
        self.url      = item['url']
        self.length   = item['content_length']

    def data(self):
        """
        Format the post data string for an File type
        """
        if os.path.exists(self.filename):
            filename = os.path.basename(self.filename)
            ctype    = self.mimetype

            if ctype is None:
                ctype = mimetypes.guess_type(self.filename)[0]

                if ctype is None:
                    ctype = 'application/octet-stream'

            mtype, stype = ctype.split('/', 1)

            msg = email.mime.base.MIMEBase(mtype, stype)
            msg.set_payload(open(self.filename, 'rb').read())
            email.encoders.encode_base64(msg)

            s = '%s\n' % MIME_BOUNDARY
            for key, value in [('note_type', self.type), ('note_body', self.body),
                               ('note_to',   self.to), ('Upload', 'Submit Query'),]:
                s += 'Content-Disposition: form-data; name="%s"\n\n%s\n%s\n' % (key, value, MIME_BOUNDARY)

            s += 'Content-Disposition: form-data; name="media_file"; filename="%s"\n\n%s\n%s\n' % (filename, msg.get_payload(), MIME_BOUNDARY)
        else:
            s = None

        return s


class PublicNotes(object):
    """
    Pubic Note List

    Requires an active Pownce Session.
    """
    def __init__(self, session=None):
        self.session  = session
        self.items    = {}
        self.feed     = None
        self.lastpoll = None
        self.error    = ''

    def get(self, filterBy=None):
        self.filter = filterBy
        self.limit  = 20
        self.page   = 0


if __name__ == '__main__':
    # yes, hackish - but it works for simple testing
    if os.path.exists('test.cfg'):
        username, password, app_key, debug_flag = open('test.cfg').read().split('\n')
        if debug_flag == 'true':
            _debug = True
            import pprint
            pp = pprint.PrettyPrinter()

    o = Session(app_key=app_key)

    o.login(username, password)

    profile = Profile(o)
    profile.refresh(True)

    profile._dump()
    profile.friends._dump()
    profile.fans._dump()
    profile.fan_of._dump()

    notes = Notes(o)
    notes.refresh()

    for note in notes.items:
        print note.type, note.id, note.body

